package com.googlecode.afx.utils;

import java.lang.reflect.Method;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.ConfigurablePropertyAccessor;
import org.springframework.beans.PropertyAccessorFactory;

/**
 * Wrapper class for reflective property and method access.
 * 
 * @author Martin
 *
 */
public class ReflectiveAccessor {
	
	private static final Log LOG = LogFactory.getLog(ReflectiveAccessor.class);
	
	private BeanWrapper wrapper;
	
	public ReflectiveAccessor(Object bean) {
		if(bean == null) {
			throw new IllegalArgumentException("Constructor argmument must not be null!");
		}
		this.wrapper = PropertyAccessorFactory.forBeanPropertyAccess(bean);
	}

	public ReflectiveAccessor(BeanWrapper wrapper) {
		if(wrapper == null) {
			throw new IllegalArgumentException("Constructor argmument must not be null!");
		}
		this.wrapper = wrapper;
	}
	
	/**
	 * Gets the property from the node located under <tt>propertyName</tt>, where this parameter can be
	 * also a nested path. 
	 * <p>
	 * This method is performing 3 strategies for accessing the property:
	 * <ul>
	 *  <li>treat <tt>propertyName</tt> as a path the really contains a method name and perform a method invocation</li>
	 *  <li>treat <tt>propertyName</tt> as a path that contains the name of a JavaBean property and call the corresponding <tt>getter</tt>.
	 *  <li>treat <tt>propertyName</tt> as a path to a field within an arbitrary object, not necessarily JavaBean.
	 * </ul>
	 * <p>
	 * This method is "inspired" by <tt>TableView.SelectionModel.selectItem</tt>: This field path contains a <tt>Property</tt> instance, but
	 * when you access the getter, you get the value (not the property), while the path <tt>TableView.SelectionModel.selectItemProperty</tt> 
	 * does not contain a <tt>Property</tt>, but an <tt>ObservableValue</tt>.
	 *  
	 * @param methodPathAndName
	 * @param expectedResultType
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public <T> T getPropertyValue(String propertyName, Class<? extends T> expectedResultType) {
		// check, if the method really exists, or if it is a JavaBean property
		if(methodExists(propertyName, this.getWrappedInstance().getClass())) {
			return this.invokeMethod(propertyName,  expectedResultType);	
		} else {
			// try to access the getter
			Object candidate1 = this.getPropertyValue(propertyName);
			if(candidate1 != null && expectedResultType.isAssignableFrom(candidate1.getClass())) {
				return (T) candidate1;
			} else {
				// try the direct field access as a last chance (required for e.g. binding to TableView.SelectionModel.selectItem)
				Object propertyProvider = getPropertyProvidingInstance(propertyName);
				ConfigurablePropertyAccessor fieldAccessor = PropertyAccessorFactory.forDirectFieldAccess(propertyProvider);
				Object candidate2 = fieldAccessor.getPropertyValue(getMethodName(propertyName));
				if(candidate2 != null && expectedResultType.isAssignableFrom(candidate2.getClass())) {
					return (T) candidate2;
				} else {
					LOG.error("Can not retrieve an instance of type '" + expectedResultType.getName() + "' under propertyName '" + propertyName + "' from class '" + this.getWrappedInstance().getClass().getName() + "'!");
					throw new IllegalStateException("Can not retrieve an instance of type '" + expectedResultType.getName() + "' under propertyName '" + propertyName + "' from class '" + this.getWrappedInstance().getClass().getName() + "'!");
				}
			}
		}		
	}
	
	/**
	 * Invoke the methods from the node located under <tt>methodPathAndName</tt>, where this parameter can be
	 * also a nested path.
	 *  
	 * @param methodPathAndName
	 * @param expectedResultType
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public <T> T invokeMethod(String methodPathAndName, Class<? extends T> expectedResultType) {
		
		Object methodProvider = this.getPropertyProvidingInstance(methodPathAndName);
		if(methodProvider == null) {
			throw new IllegalStateException("Can not retrieve methodProvider for path '" + methodPathAndName + "'! Is there a configuration error?");
		}
		Method method = getMethod(methodPathAndName, methodProvider.getClass());
		if(method == null) {
			throw new IllegalStateException("Can not retrieve method for path '" + methodPathAndName + "'! Is there a configuration error?");
		}
		Object candidate = ReflectionUtils.invokeMethod(method, methodProvider);
		if(candidate == null) {
			return null;
		}
		if(!(expectedResultType.isAssignableFrom(candidate.getClass()))) {
			LOG.error("Return value of method '" + method.getName() + "' in class '" + methodProvider.getClass().getName() + "' is not of required type '" + expectedResultType.getName() + "', but of type '" + candidate.getClass().getName() + "'!");
			throw new IllegalStateException("Return value of method '" + method.getName() + "' in class '" + methodProvider.getClass().getName() + "' is not of required type '" + expectedResultType.getName() + "', but of type '" + candidate.getClass().getName() + "'!");				
		}
		return (T) candidate;
	}

	/**
	 * Returns the value of the property under <tt>propertyName</tt>. 
	 *  
	 * @param propertyPath
	 * @return
	 */
	public Object getPropertyValue(String propertyName) {
		return this.wrapper.getPropertyValue(propertyName);
	}
	
	/**
	 * Returns the type of the property under <tt>propertyName</tt>. 
	 *  
	 * @param propertyPath
	 * @return
	 */
	public Object getPropertyType(String propertyName) {
		return this.wrapper.getPropertyType(propertyName);
	}

	/**
	 * Returns the wrapped instance.
	 * @return
	 */
	public Object getWrappedInstance() {
		return this.wrapper.getWrappedInstance();
	}

	/**
	 * Determine the instance that provides the property to retrieve. For nested paths, that is the last
	 * path item before the last ".".
	 * 
	 * @param methodPathAndName
	 * @return
	 */
	private Object getPropertyProvidingInstance(String methodPathAndName) {
		int lastIndex = methodPathAndName.lastIndexOf('.');
		if(lastIndex > 0 && lastIndex != methodPathAndName.length() - 1) {
			String path = methodPathAndName.substring(0, lastIndex);
			return wrapper.getPropertyValue(path);
		}
		if(lastIndex == -1) {
			return wrapper.getWrappedInstance();
		}
		return null;
	}

	/**
	 * Determines the method name within parameter <tt>methodPathAndName</tt>.
	 * @param methodPathAndName
	 * @return
	 */
	private static String getMethodName(String methodPathAndName) {
		int lastIndex = methodPathAndName.lastIndexOf('.');
		if(lastIndex > 0 && lastIndex != methodPathAndName.length() - 1) {
			return methodPathAndName.substring(lastIndex+1);
		}
		if(lastIndex == -1) {
			return methodPathAndName;
		}
		return null;
	}

	
	/**
	 * Retrieve the method from the potentially nested path under <tt>methodPathAndName</tt>.
	 * 	
	 * @param methodPathAndName
	 * @return
	 */
	public static Method getMethod(String methodPathAndName, Class<?> clazz) {
		String methodName = getMethodName(methodPathAndName);
		if(methodName == null) {
			return null;
		}
		return ReflectionUtils.findMethod(clazz, methodName);
	}

	/**
	 * Checks, whether the return type of the method specified by <tt>methodPathAndName</tt> contained in
	 * class <tt>clazz</tt> is of type <tt>returnType</tt>.
	 * 
	 * @param methodPathAndName
	 * @param clazz
	 * @param returnType
	 * @return
	 */
	public static boolean checkMethodReturnType(String methodPathAndName, Class<?> clazz, Class<?> returnType) {
		Method method = getMethod(methodPathAndName, clazz);
		if(method == null) {
			return false;
		}
		return returnType.isAssignableFrom(method.getReturnType());
	}
	
	/**
	 * Checks whether the method specified by <tt>methodPathAndName</tt> exists in
	 * class <tt>clazz</tt>.
	 *  
	 * @param methodPathAndName
	 * @param clazz
	 * @return
	 */
	public static boolean methodExists(String methodPathAndName, Class<?> clazz) {
		return getMethod(methodPathAndName, clazz) != null;
	}

}
